<?php

/**
 * SmartPHP framework
 *
 * @name SmartPHP framework
 * @package SmartPHP
 * @author 费尔 <admin@smartphp.cn>
 * @version 1.0 Beta
 * @link http://www.smartphp.cn
 * @copyright smartphp.cn
 * @license <a href="http://www.smartphp.cn">smartphp</a>
 */

namespace Base;

use \PDO,
    \PDOException;

/**
 * Model
 *
 * @author 费尔 <admin@smartphp.cn>
 */
class Model {

    //连接DNS
    protected $dsn;
    //用户名
    protected $user;
    //密码
    protected $password;
    //具体驱动的连接选项
    protected $params = [
        PDO::ATTR_CASE => PDO::CASE_NATURAL,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_EMULATE_PREPARES => false,
    ];
    //表名称
    protected $table;
    // 表完成名称
    protected $tableName;
    // 表名前缀
    protected $tablePrefix;
    //字段引用符号
    protected $keyQuote = '``';
    //缓存
    protected $cache = null;
    //最后一条查询
    private $lastQuery = null;
    //最后一个错误
    private $lastError = null;
    //显示错误
    protected $showErrors = true;
    //返回最后插入的主键ID
    public $insertId;
    private $dbh, $driver;
    //查询参数
    protected $options = [];
    //SQL表达式
    private $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%';
    private $insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
    private $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES %DATA% %COMMENT%';
    private $updateSql = 'UPDATE %TABLE% SET %SET% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
    private $deleteSql = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';

    function __construct($table = '', $prefix = '', $dsn = '', $user = '', $password = '', $params = [], $keyquote = '') {
        $this->init($table, $prefix, $dsn, $user, $password, $params, $keyquote);
    }

    private function init($table = '', $prefix = '', $dsn = '', $user = '', $password = '', $params = [], $keyquote = '') {
        if (!$this->table)
            $this->table = $table ?: $this->getModelName();
        $this->dsn($dsn ?: Config::get('database.dsn'));
        $this->user($user ?: Config::get('database.user'));
        $this->password($password ?: Config::get('database.password'));
        $this->params($params ?: Config::get('database.params'));
        $this->prefix($prefix ?: Config::get('database.prefix'));
        $this->keyQuote($keyquote ?: Config::get('database.keyquote'));
//        if (!$this->dbh)
//            $this->connect($this->dsn(), $this->user(), $this->password(), $this->params());
    }

    public function dsn($dsn = '') {
        if ($dsn !== '')
            $this->dsn = $dsn ?: null;
        else
            return $this->dsn;
        return $this;
    }

    public function user($user = '') {
        if ($user !== '')
            $this->user = $user ?: null;
        else
            return $this->user;
        return $this;
    }

    public function password($password = '') {
        if ($password !== '')
            $this->password = $password ?: null;
        else
            return $this->password;
        return $this;
    }

    public function params($params = '') {
        if ($params !== '')
            $this->params = $params ?: null;
        else
            return $this->params;
        return $this;
    }

    public function prefix($prefix = '') {
        if ($prefix !== '')
            $this->tablePrefix = $prefix ?: null;
        else
            return $this->tablePrefix;
        return $this;
    }

    public function keyQuote($keyQuote = '') {
        if ($keyQuote !== '')
            $this->keyQuote = $keyQuote ?: null;
        else
            return $this->keyQuote;
        return $this;
    }

    /**
     * 连接数据库
     * @access public
     * @param string $dsn
     * @param string $user
     * @param string $password
     * @param array $params
     */
    public function connect($dsn = '', $user = '', $password = '', $params = []) {
        static $pdo = [];
        if (!$dsn)
            $dsn = $this->dsn;
        if ($dsn && !$this->dbh) {
            try {
                if (is_array($dsn))
                    $dsn = $dsn[floor(mt_rand(0, count($dsn) - 1))];
                $this->dbh = $pdo[$dsn] ?? $pdo[$dsn] = new PDO($dsn, $user ?: $this->user, $password ?: $this->password, $params ? (array) $params : $this->params);
                //$this->driver = $this->getDriverName();
                foreach (Config::get('database.commands') as $value) {
                    $this->dbh->exec($value);
                }
                $this->driver(strstr($dsn, ':', true));
                if (in_array($this->driver(), ['mariadb', 'mysql'])) {
                    $this->dbh->exec('SET SQL_MODE=ANSI_QUOTES');
                }
            } catch (PDOException $e) {
                trigger_error(trim($e->getMessage()));
            }
        }
    }

    /**
     * 查询SQL
     * @access public
     * @param type $query
     * @return boolean|int|array
     */
    public function query($query = '') {

        if (!$this->dbh) {
            $this->connect($this->dsn(), $this->user(), $this->password(), $this->params());
            if (!$this->dbh)
                return false;
        }

        if (empty($query))
            return $this->dbh;


        $query = str_replace("/[\n\r]/", '', trim($query));
        $this->lastQuery = $query;
        try {
            $type = $this->driver();
            if (preg_match("/^(insert|delete|update|replace|drop|create)\s+/i", $query)) {
                $start = microtime(true);
                $returnVal = $this->dbh->exec($query);
                $time = App::getUsageTime($start);
                if ($this->getError())
                    return false;
                $this->insertId = preg_match("/^(insert|replace)\s+/i", $query) ? $this->dbh->lastInsertId() : 0;
            } else {
                $start = microtime(true);
                $returnVal = $this->getCache($query);
                $time = App::getUsageTime($start);
                if (!$returnVal) {
                    $start = microtime(true);
                    $sth = $this->dbh->query($query);
                    $time = App::getUsageTime($start);
                    if ($this->getError())
                        return false;
                    $returnVal = $sth->fetchAll(PDO::FETCH_ASSOC);
                    $this->setCache($query, $returnVal);
                }else {
                    $type = 'cache';
                }
                if ($this->cache !== false)
                    $this->cache = null;
            }
            Log::sql("[{$time}][{$type}]{$query}");
        } catch (PDOException $e) {
            $this->lastError = $e->getMessage();
            $this->showErrors ? trigger_error($e->getMessage()) : null;
        }
        return $returnVal;
    }

    /**
     * SQL指令安全过滤
     * @access public
     * @param mixed $value
     * @return mixed
     */
    public function quote($value) {
        if (is_bool($value))
            $value = $value ? 1 : 0;
        elseif (is_null($value))
            $value = 'null';
        elseif (is_int($value))
            $value = intval($value);
        else
            $value = $this->query()->quote($value);
        return $value;
    }

    /**
     * 数组时SQL指令安全过滤
     * @access public
     * @param mixed $array
     * @return mixed
     */
    protected function arrayQuote($array) {
        $temp = [];
        foreach ($array as $value) {
            $temp[] = $this->quote($value);
        }
        return implode($temp, ',');
    }

    protected function fnQuote($column, $string) {
        return (strpos($column, '#') === 0 && preg_match('/^[A-Z0-9\_]*\([^)]*\)$/', $string)) ? $string : $this->quote($string);
    }

    /**
     * 得到当前的数据对象名称
     *
     * @access public
     * @return string
     */
    public function getModelName() {
        if (!$this->table) {
            $name = substr(get_class($this), strlen('Model'));
            if (($pos = strpos($name, '\\')) !== false)
                $this->table = substr($name, $pos + 1);
            else
                $this->table = $name;
        }
        return $this->table;
    }

    /**
     * 得到完整的数据表名
     *
     * @access public
     * @return string
     */
    public function getTableName() {
        if (!$this->tableName) {
            $tableName = !empty($this->tablePrefix) ? $this->tablePrefix : '';
            $tableName .= $this->parseName($this->table);
            $this->tableName = strtolower($tableName);
        }
        return $this->tableName;
    }

    /**
     * 字符串命名风格转换
     *
     * @param string $name 字符串
     * @return string
     */
    public function parseName($name) {
        return Route::parseName($name);
    }

    /**
     * 返回数据库服务信息
     * @access public
     * @return array
     * [server] => Uptime: 3592  Threads: 1  Questions: 245  Slow queries: 0  Opens: 66  Flush tables: 1  Open tables: 59  Queries per second avg: 0.068
     * [driver] => mysql
     * [client] => mysqlnd 5.0.12-dev - 20150407 - $Id: 7e72f9690b1498a1bead7a637c33a831c0d2f655 $
     * [version] => 5.5.42
     * [connection] => Localhost via UNIX socket
     */
    public function getServerInfo() {
        if ($this->dbh) {
            $output = ['server' => 'SERVER_INFO', 'driver' => 'DRIVER_NAME', 'client' => 'CLIENT_VERSION', 'version' => 'SERVER_VERSION', 'connection' => 'CONNECTION_STATUS'];
            foreach ($output as $key => $value)
                $output[$key] = $this->dbh->getAttribute(constant('PDO::ATTR_' . $value));
            return $output;
        } else {
            return null;
        }
    }

    /**
     * 获取当前数据库的驱动类型
     * @access public
     * @return string
     */
    public function driver($driver = '') {
        return $driver ? $this->driver = $driver : $this->driver;
    }

    /**
     * 获取PDO对象
     * @access public
     * @return PDO|false
     */
    public function getPdo() {
        return $this->dbh ?: false;
    }

    /**
     * 返回获取第一列
     *
     * @param array     $options 表达式
     * @return array
     */
    public function fetchColumn($options = []) {
        return array_values($this->query($this->limit(1)->parseSelect($options ?: $this->options))[0])[0] ?? 0;
    }

    /**
     * 查询一条数据
     *
     * @param array     $options 表达式
     * @return array
     */
    public function find($options = []) {
        return $this->query($this->limit(1)->parseSelect($options ?: $this->options))[0] ?? false;
    }

    /**
     * 查询多条数据
     *
     * @param array     $options 表达式
     * @return array
     */
    public function findAll($options = []) {
        return $this->query($this->parseSelect($options ?: $this->options));
    }

    /**
     * 统计行数
     *
     * @param string $column
     * @return int
     */
    public function count($column = '*', $fun = '') {
        return $this->field('COUNT(' . $fun . $this->parseKey($column) . ')')->fetchColumn();
    }

    /**
     * 返回最大值
     *
     * @param string $column
     * @return int
     */
    public function max($column, $fun = '') {
        return $this->field('MAX(' . $fun . $this->parseKey($column) . ')')->fetchColumn();
    }

    /**
     * 返回最小值
     *
     * @param string $column
     * @return int
     */
    public function min($column, $fun = '') {
        return $this->field('MIN(' . $fun . $this->parseKey($column) . ')')->fetchColumn();
    }

    /**
     * 返回平均值
     *
     * @param string $column
     * @return int
     */
    public function avg($column, $fun = '') {
        return $this->field('AVG(' . $fun . $this->parseKey($column) . ')')->fetchColumn();
    }

    /**
     * 统计总和
     *
     * @param string $column
     * @return int
     */
    public function sum($column, $fun = '') {
        return $this->field('SUM(' . $fun . $this->parseKey($column) . ')')->fetchColumn();
    }

    /**
     * 新增数据
     * @access public
     * @param array     $data 数据
     * @param bool      $replace 是否replace
     * @param array $options 表达式
     * @return int
     */
    public function add($replace = false, $options = []) {
        if (empty($options))
            $options = $this->options;
        $sql = $this->parseInsert(($options['data'] ?? []), $replace, $options);
        return $sql ? ($this->query($sql) ? $this->insertId : 0) : 0;
    }

    /**
     * 新增数据
     * @access public
     * @param array     $data 数据
     * @param bool      $replace 是否replace
     * @param array $options 表达式
     * @return int
     */
    public function insert($replace = false, $options = []) {
        return $this->add($replace, $options);
    }

    /**
     * 批量新增数据
     * @access public
     * @param bool      $replace 是否replace
     * @param array     $options 表达式
     * @return int
     */
    public function addAll($replace = false, $options = []) {
        if (empty($options))
            $options = $this->options;
        $sql = $this->parseInsertAll($options['data'] ?? [], $replace, $options);
        return $sql ? $this->query($sql) : 0;
    }

    /**
     * 批量新增数据
     * @access public
     * @param bool      $replace 是否replace
     * @param array     $options 表达式
     * @return int
     */
    public function insertAll($replace = false, $options = []) {
        return $this->add($replace, $options);
    }

    /**
     * 更新数据
     * @access public
     * @param array     $options 表达式
     * @return int
     */
    public function update($options = []) {
        if (empty($options))
            $options = $this->options;
        $sql = $this->parseUpdate($options['data'] ?? [], $options);
        return $sql ? $this->query($sql) : 0;
    }

    /**
     * 更新数据
     * @access public
     * @param array     $options 表达式
     * @return int
     */
    public function save($options = []) {
        return $this->update($options);
    }

    /**
     * 删除数据
     * @access public
     * @param array $options 表达式
     * @return int
     */
    public function delete($options = []) {
        return $this->query($this->parseDelete($options ?: $this->options));
    }

    /**
     * 生成查询SQL
     * @access public
     * @param array $options 表达式
     * @return string
     */
    public function parseSelect($options = []) {
        if (empty($options))
            $options = $this->options;
        $sql = str_replace(
                ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'], [
            $this->parseTable($options['table'] ?? '', $options['alias'] ?? ''),
            $this->parseDistinct($options['distinct'] ?? ''),
            $this->parseField($options['field'] ?? ''),
            $this->parseJoin($options['join'] ?? ''),
            $this->parseWhere($options['where'] ?? ''),
            $this->parseGroup($options['group'] ?? ''),
            $this->parseHaving($options['having'] ?? ''),
            $this->parseOrder($options['order'] ?? ''),
            $this->parseLimit($options['limit'] ?? ''),
            $this->parseUnion($options['union'] ?? ''),
            $this->parseLock($options['lock'] ?? ''),
            $this->parseComment($options['comment'] ?? ''),
            $this->parseForce($options['force'] ?? '')
                ], $this->selectSql);
        $this->flush();
        return $sql;
    }

    /**
     * 生成insert SQL
     * @access public
     * @param array     $data 数据
     * @param bool      $replace 是否replace
     * @param array $options 表达式
     * @return string
     */
    public function parseInsert(array $data, $replace = false, $options = []) {
        if (empty($data)) {
            return 0;
        }
        $data = $this->parseData($data);

        $fields = array_keys($data);
        $values = array_values($data);
        if (empty($options))
            $options = $this->options;
        $sql = str_replace(
                ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [
            $replace ? 'REPLACE' : 'INSERT',
            $this->parseTable($options['table'] ?? '', $options['alias'] ?? ''),
            implode(' , ', $fields),
            implode(' , ', $values),
            $this->parseComment($options['comment'] ?? ''),
                ], $this->insertSql);
        $this->flush();
        return $sql;
    }

    /**
     * 生成insertall SQL
     * @access public
     * @param array     $dataSet 数据集
     * @param array     $options 表达式
     * @return string
     */
    public function parseInsertAll(array $dataSet, $replace = false, $options = []) {
        if (empty($options))
            $options = $this->options;
        $values = [];
        foreach ($dataSet as $data) {
            $value = [];
            foreach ($data as $val) {
                $value[] = $this->parseValue($val);
            }
            $values[] = '(' . implode(',', array_values($value)) . ')';
        }
        $fields = array_map([$this, 'parseKey'], array_keys(reset($dataSet)));
        $sql = str_replace(
                ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [
            $replace ? 'REPLACE' : 'INSERT',
            $this->parseTable($options['table'] ?? '', $options['alias'] ?? ''),
            implode(' , ', $fields),
            implode(' , ', $values),
            $this->parseComment($options['comment'] ?? ''),
                ], $this->insertAllSql);
        $this->flush();
        return $sql;
    }

    /**
     * 生成update SQL
     * @access public
     * @param array     $data 数据
     * @param array     $options 表达式
     * @return string
     */
    public function parseUpdate(array $data, $options = []) {
        if (empty($options))
            $options = $this->options;
        $set = $this->parseSetData($data);
        if (empty($set)) {
            return 0;
        }
        $sql = str_replace(
                ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], [
            $this->parseTable($options['table'] ?? '', $options['alias'] ?? ''),
            implode(',', $set),
            $this->parseJoin($options['join'] ?? ''),
            $this->parseWhere($options['where'], $options['table'] ?? ''),
            $this->parseOrder($options['order'] ?? ''),
            $this->parseLimit($options['limit'] ?? ''),
            $this->parseLimit($options['lock'] ?? ''),
            $this->parseComment($options['comment'] ?? '')
                ], $this->updateSql);
        $this->flush();
        return $sql;
    }

    /**
     * 生成delete SQL
     * @access public
     * @param array $options 表达式
     * @return string
     */
    public function parseDelete($options = []) {
        if (empty($options))
            $options = $this->options;
        $sql = str_replace(
                ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], [
            $this->parseTable($options['table'] ?? '', $options['alias'] ?? ''),
            !empty($options['using'] ?? '') ? ' USING ' . $this->parseTable($options['using'] ?? '') . ' ' : '',
            $this->parseJoin($options['join'] ?? ''),
            $this->parseWhere($options['where'] ?? ''),
            $this->parseOrder($options['order'] ?? ''),
            $this->parseLimit($options['limit'] ?? ''),
            $this->parseLimit($options['lock'] ?? ''),
            $this->parseComment($options['comment'] ?? '')
                ], $this->deleteSql);
        $this->flush();
        return $sql;
    }

    /**
     * 查询过后清空sql表达式组装 避免影响下次查询
     */
    public function flush() {
        $this->options = [];
    }

    /**
     * 数据分析
     * @access protected
     * @param array     $data 数据
     * @param array     $options 查询参数
     * @return array
     */
    protected function parseSetData($data) {
        if (empty($data)) {
            return [];
        }
        $set = [];
        foreach ($data as $key => $val) {
            $item = $this->parseKey($key);
            preg_match('/([\w]+)(\[(\+|\-|\*|\/)\])?/i', $key, $match);
            if (isset($match[3])) {
                if (is_numeric($val))
                    $fields[] = $this->parseKey($match[1]) . ' = ' . $this->parseKey($match[1]) . ' ' . $match[3] . ' ' . $value;
            } elseif (isset($val[0]) && 'exp' == $val[0]) {
                $set[] = $item . '=' . (string) $val[1];
            } else {
                $set[] = $item . '=' . $this->quote($val);
            }
        }
        return $set;
    }

    /**
     * 数据分析
     * @access protected
     * @param array     $data 数据
     * @param array     $options 查询参数
     * @return array
     */
    protected function parseData($data) {
        if (empty($data)) {
            return [];
        }
        $result = [];
        foreach ($data as $key => $val) {
            $item = $this->parseKey($key);
            if (isset($val[0]) && 'exp' == $val[0]) {
                $result[$item] = (string) $val[1];
            } else {
                $result[$item] = $this->quote($val);
            }
        }
        return $result;
    }

    /**
     * 字段名分析
     * @access protected
     * @param string $key
     * @return string
     */
    protected function parseKey($key) {
        $l = $this->keyQuote{0};
        $r = $this->keyQuote{1};
        return str_replace($l . '*' . $r, '*', $l . str_replace('.', $r . '.' . $l, preg_replace('/(^#|\(JSON\)\s*)/', '', $key)) . $r);
    }

    /**
     * value分析
     * @access protected
     * @param mixed     $value
     * @param string    $field
     * @return string|array
     */
    protected function parseValue($value) {
        return is_array($value) ? array_map([$this, 'parseValue'], $value) : $this->quote($value);
    }

    /**
     * 表别名
     *
     * @param string $alias
     * @return Model
     */
    public function alias($alias) {
        $this->options['alias'] = $alias;
        return $this;
    }

    /**
     * field分析
     * @access protected
     * @param mixed $fields
     * @return string
     */
    protected function parseField($fields) {
        if ('*' == $fields || empty($fields)) {
            $fieldsStr = '*';
        } elseif (is_array($fields)) {
            // 支持 'field1'=>'field2' 这样的字段别名定义
            $array = [];
            foreach ($fields as $key => $field) {
                if (!is_numeric($key)) {
                    $array[] = $this->parseKey($key) . ' AS ' . $this->parseKey($field);
                } else {
                    $array[] = $this->parseKey($field);
                }
            }
            $fieldsStr = implode(',', $array);
        } else {
            $fields = preg_split('/\s*,\s*/', trim($fields), -1, PREG_SPLIT_NO_EMPTY);
            foreach ($fields as $i => $column) {
                if (strpos($column, '(') === false) {
                    if (preg_match('/^(.*?)\s+AS\s+(\w+)$/im', $column, $match))
                        $fields[$i] = $this->parseKey($match[1]) . ' AS ' . $this->parseKey($match[2]);
                    elseif (preg_match('/^(.*?)\s+\.\s+(\w+)$/im', $column, $match))
                        $fields[$i] = $this->parseKey($match[1]) . '.' . ($match[2] == '*' ? $match[2] : $this->parseKey($match[2]));
                    else
                        $fields[$i] = $this->parseKey($column);
                } else
                    $fields[$i] = (string) $column;
            }
            $fieldsStr = implode($fields, ',');
        }
        return $fieldsStr;
    }

    /**
     * table分析
     * @access protected
     * @param mixed $table
     * @return string
     */
    protected function parseTable($tables, $alias = '') {
        if (empty($tables))
            $tables = $this->getTableName();
        if (empty($tables))
            trigger_error('Not Table');
        if (!is_array($tables)) {
            $tables = $this->parseSqlTable($tables);
            $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
        }
        foreach ($tables as $i => $table) {
            if (strpos($table, '(') === false) {
                if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches))
                    $tables[$i] = $this->parseKey($matches[1]) . ' AS ' . $this->parseKey($matches[2]);
                else
                    $tables[$i] = $this->parseKey($table) . ($alias ? ' AS ' . $this->parseKey($alias) : '');
            }
        }
        return implode(',', $tables);
    }

    /**
     * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名（小写）
     * @access public
     * @param string $sql sql语句
     * @return string
     */
    public function parseSqlTable($sql) {
        if (false !== strpos($sql, '__')) {
            $prefix = $this->tablePrefix;
            $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) {
                return $prefix . strtolower($match[1]);
            }, $sql);
        }
        return $sql;
    }

    /**
     * where分析
     * @access protected
     * @param mixed $where
     * @return string
     */
    protected function parseWhere($where) {
        if (empty($where))
            return '';

        $whereStr = array_map(function ($value) {
            $whereStr = '';
            if (is_array($value)) {
                if (!empty($value))
                    $whereStr = '( ' . $this->buildWhere($value, ' AND') . ' )';
            } else {
                $whereStr = $value;
            }
            return $whereStr;
        }, $where);
        if (!empty($whereStr))
            $whereStr = implode(' AND ', $whereStr);
        return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
    }

    protected function buildWhere($where, $conjunctor, $outerConjunctor = null) {
        foreach ($where as $key => $value) {
            $type = gettype($value);
            if (preg_match("/^(AND|OR)(\s+#.*)?$/i", $key, $match) && $type == 'array') {
                $wheres[] = 0 !== count(array_diff_key($value, array_keys(array_keys($value)))) ?
                        '(' . $this->buildWhere($value, ' ' . strtoupper($match[1])) . ')' :
                        '(' . $this->innerConjunct($value, ' ' . strtoupper($match[1]), $conjunctor) . ')';
            } else {
                preg_match('/(#?)([\w\.\-\|&]+)(\[(\>|\>\=|\<|\<\=|\!|\`|\<\>|\>\<|\!?~)\])?/i', $key, $match);
                if (strpos($match[2], '|')) {
                    $array = explode('|', $match[2]);
                    $str = [];
                    foreach ($array as $k)
                        $str[] = $this->buildWhere([$k . $match[3] => $value], ' AND');
                    $wheres[] = '( ' . implode(' OR ', $str) . ' )';
                } elseif (strpos($match[2], '&')) {
                    $array = explode('&', $match[2]);
                    $str = [];
                    foreach ($array as $k)
                        $str[] = $this->buildWhere([$k . $match[3] => $value], ' AND');
                    $wheres[] = '( ' . implode(' AND ', $str) . ' )';
                } else {
                    $column = $this->parseKey($match[2]);
                    if (isset($match[4])) {
                        $operator = $match[4];
                        if ($operator == '`') {
                            $wheres[] = 'FIND_IN_SET(' . $this->quote($value) . ',' . $column . ')';
                        } elseif ($operator == '!') {
                            switch ($type) {
                                case 'NULL':
                                    $wheres[] = $column . ' IS NOT NULL';
                                    break;
                                case 'array':
                                    $wheres[] = $column . ' NOT IN (' . $this->arrayQuote($value) . ')';
                                    break;
                                case 'integer':
                                case 'double':
                                    $wheres[] = $column . ' != ' . $value;
                                    break;
                                case 'boolean':
                                    $wheres[] = $column . ' != ' . ($value ? '1' : '0');
                                    break;
                                case 'string':
                                    $wheres[] = $column . ' != ' . $this->fnQuote($key, $value);
                                    break;
                            }
                        } elseif ($operator == '<>' || $operator == '><') {
                            if ($type == 'array') {
                                if ($operator == '><') {
                                    $column .= ' NOT';
                                }
                                if (is_numeric($value[0]) && is_numeric($value[1])) {
                                    $wheres[] = '(' . $column . ' BETWEEN ' . $value[0] . ' AND ' . $value[1] . ')';
                                } else {
                                    $wheres[] = '(' . $column . ' BETWEEN ' . $this->quote($value[0]) . ' AND ' . $this->quote($value[1]) . ')';
                                }
                            }
                        } elseif ($operator == '~' || $operator == '!~') {
                            if ($type != 'array') {
                                $value = (array) $value;
                            }
                            $likeClauses = [];
                            foreach ($value as $item) {
                                $item = strval($item);
                                //$suffix = mb_substr($item, -1, 1);
                                if (preg_match('/^(?!(%|\[|_])).+(?<!(%|\]|_))$/', $item)) {
                                    $item = '%' . $item . '%';
                                }
                                $likeClauses[] = $column . ($operator === '!~' ? ' NOT' : '') . ' LIKE ' . $this->fnQuote($key, $item);
                            }
                            $wheres[] = implode(' OR ', $likeClauses);
                        } elseif (in_array($operator, array('>', '>=', '<', '<='))) {
                            if (is_numeric($value)) {
                                $wheres[] = $column . ' ' . $operator . ' ' . $value;
                            } elseif (strpos($key, '#') === 0) {
                                $wheres[] = $column . ' ' . $operator . ' ' . $this->fnQuote($key, $value);
                            } else {
                                $wheres[] = $column . ' ' . $operator . ' ' . $this->quote($value);
                            }
                        }
                    } else {
                        switch ($type) {
                            case 'NULL':
                                $wheres[] = $column . ' IS NULL';
                                break;
                            case 'array':
                                $wheres[] = $column . ' IN (' . $this->arrayQuote($value) . ')';
                                break;
                            case 'integer':
                            case 'double':
                                $wheres[] = $column . ' = ' . $value;
                                break;
                            case 'boolean':
                                $wheres[] = $column . ' = ' . ($value ? '1' : '0');
                                break;
                            case 'string':
                                $wheres[] = $column . ' = ' . $this->fnQuote($key, $value);
                                break;
                        }
                    }
                }
            }
        }

        return implode($conjunctor . ' ', $wheres);
    }

    protected function innerConjunct($data, $conjunctor, $outerConjunctor) {
        $haystack = array();
        foreach ($data as $value) {
            $haystack[] = '(' . $this->buildWhere($value, $conjunctor) . ')';
        }
        return implode($outerConjunctor . ' ', $haystack);
    }

    /**
     * limit分析
     * @access protected
     * @param mixed $limit
     * @return string
     */
    protected function parseLimit($limit) {
        // 根据页数计算limit
        if (isset($this->options['page'])) {
            list( $page, $listRows ) = $this->options['page'];
            $page = $page > 0 ? $page : 1;
            $listRows = $listRows > 0 ? $listRows : (is_numeric($this->options['limit']) ? $this->options['limit'] : 20);
            $offset = $listRows * ($page - 1);
            $limit = $offset . ',' . $listRows;
        }
        if (empty($limit))
            return '';
        if (is_numeric($limit))
            $limit = ' LIMIT ' . $limit;
        elseif (($limit = explode(',', $limit)) && is_numeric($limit[0]) && is_numeric($limit[1])) {
            if ($this->driver() === 'pgsql')
                $limit = ' OFFSET ' . $limit[0] . ' LIMIT ' . $limit[1];
            else
                $limit = ' LIMIT ' . $limit[0] . ',' . $limit[1];
        }
        return !empty($limit) ? $limit : '';
    }

    /**
     * join分析
     * @access protected
     * @param mixed $join
     * @return string
     */
    protected function parseJoin($join) {
        $joinStr = '';
        if (!empty($join)) {
            $joinStr = ' ' . implode(' ', $join) . ' ';
        }
        return $joinStr;
    }

    /**
     * order分析
     * @access protected
     * @param mixed $order
     * @return string
     */
    protected function parseOrder($order) {
        if (empty($order))
            return '';
        if (is_array($order)) {
            $array = [];
            foreach ($order as $key => $val) {
                if (is_numeric($key)) {
                    if (false === strpos($val, '(')) {
                        $array[] = $this->parseKey($val);
                    } elseif ('[rand]' == $val) {
                        $array[] = $this->parseRand();
                    }
                } else {
                    $array[] = $this->parseKey($key) . ' ' . (in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : '');
                }
            }
            $order = implode(',', $array);
        } else {
            $order = preg_split('/\s*,\s*/', trim($order), -1, PREG_SPLIT_NO_EMPTY);
            foreach ($order as $i => $column) {
                if (strpos($column, '(') === false) {
                    if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches))
                        $order[$i] = $this->parseKey($matches[1]) . ' ' . strtoupper($matches[2]);
                    else
                        $order[$i] = $this->parseKey($column);
                }
            }

            $order = implode(', ', $order);
        }
        return !empty($order) ? ' ORDER BY ' . $order : '';
    }

    /**
     * group分析
     * @access protected
     * @param mixed $group
     * @return string
     */
    protected function parseGroup($group) {
        if (empty($group))
            return '';
        if (!is_array($group))
            $group = preg_split('/\s*,\s*/', trim($group), -1, PREG_SPLIT_NO_EMPTY);
        foreach ($group as $i => $column) {
            if (strpos($column, '(') === false)
                $group[$i] = $this->parseKey($column);
        }
        return !empty($group) ? ' GROUP BY ' . implode(',', $group) : '';
    }

    /**
     * having分析
     * @access protected
     * @param string $having
     * @return string
     */
    protected function parseHaving($having) {
        return !empty($having) ? ' HAVING ' . $having : '';
    }

    /**
     * comment分析
     * @access protected
     * @param string $comment
     * @return string
     */
    protected function parseComment($comment) {
        return !empty($comment) ? ' /* ' . $comment . ' */' : '';
    }

    /**
     * distinct分析
     * @access protected
     * @param mixed $distinct
     * @return string
     */
    protected function parseDistinct($distinct) {
        return !empty($distinct) ? ' DISTINCT ' : '';
    }

    /**
     * union分析
     * @access protected
     * @param mixed $union
     * @return string
     */
    protected function parseUnion($union) {
        if (empty($union)) {
            return '';
        }
        $type = $union['type'];
        unset($union['type']);
        $sql = [];
        foreach ($union as $u) {
            $sql[] = $type . ' ' . $this->parseSqlTable($u);
        }
        return implode(' ', $sql);
    }

    /**
     * index分析，可在操作链中指定需要强制使用的索引
     * @access protected
     * @param mixed $index
     * @return string
     */
    protected function parseForce($index) {
        if (empty($index)) {
            return '';
        }
        if (is_array($index)) {
            $index = join(",", $index);
        }

        return sprintf(" FORCE INDEX ( %s ) ", $index);
    }

    /**
     * 设置锁机制
     * @access protected
     * @param bool $locl
     * @return string
     */
    protected function parseLock($lock = false) {
        return $lock ? ' FOR UPDATE ' : '';
    }

    /**
     * 设置数据用于新增或更新
     * @access public
     * @param mixed $data
     * @return Model
     */
    public function data(array $data) {
        $this->options['data'] = $data;
        return $this;
    }

    /**
     * 指定查询字段 支持字段排除
     *
     * @access public
     * @param mixed $field
     * @return Model
     */
    public function field($field = '*') {
        $this->options['field'] = $field ?: '*';
        return $this;
    }

    /**
     * group配制
     *
     * @param string $group
     * @return Model
     */
    public function group($group) {
        $this->options['group'] = $group;
        return $this;
    }

    /**
     * having配制
     *
     * @param string $having
     * @return Model
     */
    public function having($having) {
        $this->options['having'] = $having;
        return $this;
    }

    /**
     * order配制
     *
     * @param string|array $order
     * @return Model
     */
    public function order($order) {
        $this->options['order'] = $order;
        return $this;
    }

    /**
     * 指定查询数量
     *
     * @access public
     * @param mixed $offset 起始位置
     * @param mixed $length 查询数量
     * @return Model
     */
    public function limit($offset, $length = NULL) {
        if (is_null($length) && strpos($offset, ','))
            list( $offset, $length ) = explode(',', $offset);
        $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : '');
        return $this;
    }

    /**
     * 指定分页
     *
     * @access public
     * @param mixed $page 页数
     * @param mixed $listRows 每页数量
     * @return Model
     */
    public function page($page, $listRows = null) {
        if (is_null($listRows) && strpos($page, ','))
            list( $page, $listRows ) = explode(',', $page);
        $this->options['page'] = [intval($page), intval($listRows)];
        return $this;
    }

    /**
     * 查询唯一不同的值
     *
     * @access public
     * @param string $distinct
     * @return Model
     */
    public function distinct($distinct) {
        $this->options['distinct'] = $distinct;
        return $this;
    }

    /**
     * 查询注释
     *
     * @access public
     * @param string $comment 注释
     * @return Model
     */
    public function comment($comment) {
        $this->options['comment'] = $comment;
        return $this;
    }

    /**
     * 设置查询的表
     *
     * @param string $table
     * @return Model
     */
    public function table($table) {
        $this->options['table'] = $table;
        return $this;
    }

    /**
     * 设置查询的表
     *
     * @param string $table
     * @return Model
     */
    public function from($table) {
        $this->table($table);
        return $this;
    }

    /**
     * 设置查询条件
     *
     * @access public
     * @param mixed $where
     * @return Model
     */
    public function where(...$where) {
        array_map(function ($var) {
            if (!empty($var))
                $this->options['where'][] = $var;
        }, $where);
        return $this;
    }

    /**
     * 设置join
     *
     * @access public
     * @param mixed $join
     * @return Model
     */
    public function join(...$join) {
        array_map(function ($var) {
            foreach ((array) $var as $_join) {
                if (!empty($_join)) {
                    $_join = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) {
                        return $this->parseKey($this->tablePrefix . strtolower($match[1]));
                    }, $_join);
                    $this->options['join'][] = false !== stripos($_join, 'JOIN') ? $_join : 'INNER JOIN ' . $_join;
                }
            }
        }, $join);
        return $this;
    }

    /**
     * 设置union
     *
     * @access public
     * @param mixed $union
     * @return Model
     */
    public function union($union) {
        $this->options['union'] = $union;
        return $this;
    }

    /**
     * 设置force
     *
     * @access public
     * @param mixed $force
     * @return Model
     */
    public function force($force) {
        $this->options['force'] = $force;
        return $this;
    }

    /**
     * 设置lock
     *
     * @access public
     * @param mixed $lock
     * @return Model
     */
    public function lock($lock = true) {
        $this->options['lock'] = $lock ? true : false;
        return $this;
    }

    /**
     * 开启缓存
     * @param boolean $cache 是否开启
     * @param int $expire 缓存时间
     * @return Model
     */
    public function cache($cache = true) {
        if ($cache)
            $this->cache = (int) (is_numeric($cache) ? $cache : Config::get('database.cache'));
        return $this;
    }

    /**
     * 设置缓存
     * @param string $sql
     * @param array $value
     */
    public function setCache($sql, $value) {
        if ($this->cache) {
            Cache::set($sql, $value);
        }
        return $value ?? false;
    }

    /**
     * 获取缓存
     * @param string $sql
     * @return boolean
     */
    public function getCache($sql) {
        if ($this->cache === null && Config::get('database.querycache') && is_int(Config::get('database.cache')))
            $this->cache((int) Config::get('database.cache'));


        if ($this->cache) {
            return Cache::get($sql) ?: false;
        }
        return false;
    }

    /**
     * 捕捉错误
     * @access private
     * @return boolean
     */
    public function getError() {
        $errorStr = 'No error info';
        $err_array = $this->dbh->errorInfo();
        if (isset($err_array[1]) && $err_array[1] != 25) {
            $errorStr = '';
            foreach ($err_array as $entry) {
                $errorStr .= $entry . ', ';
            }
            $errorStr = substr($errorStr, 0, -2);
            $this->showErrors ? trigger_error($errorStr . ' ' . $this->lastQuery) : null;
            return true;
        }
        return false;
    }

    /**
     * 实例模型
     * @access public
     * @param string $table
     * @param string $prefix
     * @param string $dsn
     * @param string $user
     * @param string $password
     * @param array $params
     * @param string $prefix
     * @param string $keyquote
     * @return model
     */
    public static function model($table = '', $prefix = '', $dsn = '', $user = '', $password = '', $params = [], $keyquote = '') {
        return Loader::model($table, $prefix, $dsn, $user, $password, $params, $keyquote);
    }

    //最后一条查询
    public function lastQuery() {
        return $this->lastQuery;
    }

    //最后一个错误
    public function lastError() {
        return $this->lastError;
    }

}
